using AvaloniaMaidVSIX.Helpers;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Task = System.Threading.Tasks.Task;

namespace AvaloniaMaidVSIX.Commands
{
    /// <summary>
    /// Command handler
    /// </summary>
    internal sealed class ExtractStyleCommand
    {
        /// <summary>
        /// Command ID.
        /// </summary>
        public const int CommandId = 256;

        /// <summary>
        /// Command menu group (command set GUID).
        /// </summary>
        public static readonly Guid CommandSet = new Guid("c9e26580-831a-47a3-aea0-78cbf1d6e7eb");

        /// <summary>
        /// VS Package that provides this command, not null.
        /// </summary>
        private readonly AsyncPackage package;

        /// <summary>
        /// Initializes a new instance of the <see cref="ExtractStyleCommand"/> class.
        /// Adds our command handlers for menu (commands must exist in the command table file)
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        /// <param name="commandService">Command service to add command to, not null.</param>
        private ExtractStyleCommand(AsyncPackage package, OleMenuCommandService commandService)
        {
            this.package = package ?? throw new ArgumentNullException(nameof(package));
            commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));

            var menuCommandID = new CommandID(CommandSet, CommandId);
            var menuItem = new MenuCommand(this.Execute, menuCommandID);
            commandService.AddCommand(menuItem);
        }

        /// <summary>
        /// Gets the instance of the command.
        /// </summary>
        public static ExtractStyleCommand Instance
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the service provider from the owner package.
        /// </summary>
        private Microsoft.VisualStudio.Shell.IAsyncServiceProvider ServiceProvider
        {
            get
            {
                return this.package;
            }
        }

        /// <summary>
        /// Initializes the singleton instance of the command.
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        public static async Task InitializeAsync(AsyncPackage package)
        {
            // Switch to the main thread - the call to AddCommand in ExtractStyleCommand's constructor requires
            // the UI thread.
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);

            OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
            Instance = new ExtractStyleCommand(package, commandService);
        }

        /// <summary>
        /// This function is the callback used to execute the command when the menu item is clicked.
        /// See the constructor to see how the menu item is associated with this function using
        /// OleMenuCommandService service and MenuCommand class.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event args.</param>
        private void Execute(object sender, EventArgs e)
        {
            ThreadHelper.ThrowIfNotOnUIThread();

            var dte = ServiceProvider.GetServiceAsync(typeof(DTE)).GetAwaiter().GetResult() as DTE2;

            Models.SelectionProjectItemInfo selectionItemInfo = null;
            try
            {
                selectionItemInfo = VSHelper.GetSelectionItemInfo(dte);

                var xml = selectionItemInfo.SelectionText;

                var xmldoc = new XmlDocument();
                xmldoc.LoadXml(xml);

                var ele = ExtractNodeStyle(xmldoc.DocumentElement);

                var result = FormatXml(ele);

                selectionItemInfo.UpdateSelection(result);

                selectionItemInfo.Dispose();
            }
            catch (Exception ex)
            {
                selectionItemInfo?.Dispose();
                VsShellUtilities.ShowMessageBox(package, ex.Message, "遭遇异常", OLEMSGICON.OLEMSGICON_CRITICAL, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
            }
        }

        private static string FormatXml(XmlElement ele)
        {         // Format the XML with indentation
            var settings = new XmlWriterSettings
            {
                 OmitXmlDeclaration = true,
                Indent = true,
                IndentChars = "  ",
                NewLineChars = "\r\n",
                NewLineHandling = NewLineHandling.Replace
            };

            string result;

            using (var stringWriter = new System.IO.StringWriter())
            using (var xmlWriter = XmlWriter.Create(stringWriter, settings))
            {
                ele.WriteTo(xmlWriter);
                xmlWriter.Flush();
                result = stringWriter.GetStringBuilder().ToString();
            }

            return result;
        }


        #region impls

        public partial class XamlPropValue
        {
            public string Property { get; set; }
            public string Value { get; set; }
        }

        public static XmlElement ExtractNodeStyle(XmlElement ele)
        {
            var isControl = IsControl(ele);
            if (isControl == false) return ele;

            var tobeStyledList = new List<XamlPropValue>();
            var atts = ele.Attributes.OfType<XmlAttribute>().ToArray();

            foreach (var att in atts)
            {
                var attName = att.Name;
                if (IsIgnoreAttribute(attName)) continue;
                var attValue = att.InnerText;
                if (IsIgnoreValue(attValue)) continue;
                ele.Attributes.Remove(att);
                tobeStyledList.Add(new XamlPropValue() { Property = att.Name, Value = att.InnerText });
            }


            if (tobeStyledList.Count != 0)
            {
                var styleNode = GetOrCreateStyle(ele);
                styleNode.AppendChild(CreateStyle(ele.OwnerDocument, ele.Name, tobeStyledList));
            }
            var children = ele.ChildNodes.OfType<XmlElement>().ToArray();

            foreach (var child in children)
            {
                ExtractNodeStyle(child);
            }

            return ele;
        }

        public static XmlElement GetOrCreateStyle(XmlElement control)
        {
            var controlClassName = control.Name;
            var styleName = controlClassName + ".Styles";

            var ele = control.GetElementsByTagName(styleName).OfType<XmlElement>().FirstOrDefault();
            if (ele is null)
            {
                ele = control.OwnerDocument.CreateElement(styleName);
                control.InsertBefore(ele, control.FirstChild);
            }
            return ele;
        }

        private static XmlElement CreateStyle(XmlDocument doc, string name, List<XamlPropValue> propValues)
        {
            var ele = doc.CreateElement("Style");
            var selector = doc.CreateAttribute("Selector");
            selector.Value = name;
            ele.Attributes.Append(selector);

            foreach (var propValue in propValues)
            {
                var child = doc.CreateElement("Setter");
                var propertyAtt = doc.CreateAttribute("Property");
                propertyAtt.Value = propValue.Property;

                var valueAtt = doc.CreateAttribute("Value");
                valueAtt.Value = propValue.Value;

                child.Attributes.Append(propertyAtt);
                child.Attributes.Append(valueAtt);

                ele.AppendChild(child);
            }

            return ele;

        }

        private static readonly string[] IgnoreAttributes = new string[]
        {
            "Name", "Key", "Classes", "Content", "ItemsSource", "Text"
        };

        private static bool IsIgnoreAttribute(string attributeName)
        {
            if (attributeName.IndexOf(':') > 0) return true;
            if (attributeName.IndexOf('.') > 0) return true;
            return IgnoreAttributes.Any(i => i == attributeName);
        }

        private static bool IsIgnoreValue(string value)
        {
            if (value.IndexOf('{') > 0) return true;
            return false;
        }

        private static readonly string[] ControlNames = new string[]
        {
            "Border", "TextBox", "TextBlock", "Button", "StackPanel", "Grid"
        };

        private static bool IsControl(XmlElement ele)
        {
            var eleName = ele.Name;
            return ControlNames.Any(i => i == eleName);
        }


        #endregion
    }
}
